本系列文已出版成書「NestJS 基礎必學實務指南:使用強大且易擴展的 Node.js 框架打造網頁應用程式」,感謝 iT 邦幫忙與博碩文化的協助。如果對 NestJS 有興趣、覺得這個系列文對你有幫助的話,歡迎前往購書,你的支持是我最大的寫作動力!
前面有介紹過 Module 的一些基本使用方式,然而有一項非常強大的功能沒有被提及,就是 動態模組(Dynamic Module),它可以用很簡單的方式去客製化 Provider 的內容,使該 Module 的 Provider 動態化,什麼意思呢?簡單來說,就是我們希望這個 Module 是可以透過外部傳入參數去設置 Provider 的內容,與一般 靜態模組(Static Module) 不同的地方在於,靜態模組建立後 Provider 即建立完畢,若要更改 Provider 相關配置則要變動這個 Module 內部的程式碼;動態模組則是將可能會變動的部分 參數化,讓使用者在使用此 Module 時,可以透過其提供的 靜態方法 來帶入參數,讓 Provider 接受該參數並建立 Module。

用生活中的例子來說明的話,靜態模組就像一個專用遙控器,在沒有去改寫內部的規則之前,它只能針對特定設備做控制;動態模組就像一個萬用遙控器,同樣是控制設備,但只需要根據特定的操作就能去控制不同的設備。
動態模組是很常使用的功能,其中,最常遇到的情境就是環境變數管理,設計一個 Module 專門處理環境變數,這樣的情境非常適合使用動態模組來處理,原因是管理環境變數的邏輯通常是不變的,會變的部分僅僅是讀取環境變數的檔案路徑等,透過動態模組的機制成功將其抽離成共用元件,降低耦合度。
注意:關於環境變數的介紹會在下篇做更詳細的說明。
這篇我們會運用動態模組與 dotenv 來實作一套簡單的環境變數管理模組,名稱定為 ConfigurationModule。
注意:
dotenv是一套用於管理環境變數的套件,詳細內容可以參考官方文件。
目標是讓 ConfigurationModule 提供一個靜態方法 forRoot,它可以接受一個包含 key 值為 path 的物件參數,path 即 .env 檔的相對路徑,透過 forRoot 將參數帶給 ConfigurationService 來處理 .env 的檔案並管理解析出來的變數。首先,透過 npm 安裝 dotenv:
$ npm install dotenv --save
透過 CLI 產生 ConfigurationModule 與 ConfigurationService:
$ nest generate module common/configuration
$ nest generate service common/configuration
接著打開 configuration.module.ts,替 ConfigurationModule 添加一個 forRoot 靜態方法,回傳的值即為 DynamicModule,而 DynamicModule 其實就是一個物件,與 @Module 裝飾器內的參數大致相同,不同的是必須要帶上 module 參數,其值為 ConfigurationModule 本身,另外,還有 global 參數可以使產生出來的 Module 變成全域:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigurationService } from './configuration.service';
@Module({})
export class ConfigurationModule {
static forRoot(): DynamicModule {
return {
providers: [
ConfigurationService
],
module: ConfigurationModule,
global: true
};
}
}
注意:靜態方法可以自行設計,但回傳值必須為同步或非同步
DynamicModule,名稱通常會使用forRoot或register。
從上方程式碼可以看出 @Module 的參數淨空了,這是為什麼呢?因為我們只使用動態模組,所以沒有特別設計靜態模組的部分,但如果要設計也是可以的。
接下來要在 forRoot 設計包含 key 值為 path 的物件參數,並將 path 取出,運用 Value Provider 的方式將該值記錄下來。先在 configuration 資料夾下新增 constants 資料夾,並在裡面建立 token.const.ts 來管理 token:
export const ENV_PATH = 'ENV_PATH';
調整 configuration.module.ts:
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigurationService } from './configuration.service';
import { ENV_PATH } from './constants/token.const';
@Module({})
export class ConfigurationModule {
static forRoot(options: { path: string }): DynamicModule {
return {
providers: [
{
provide: ENV_PATH,
useValue: options.path
},
ConfigurationService
],
exports: [
ConfigurationService
],
module: ConfigurationModule,
global: true
};
}
}
最後就是設計 ConfigurationService 的內容了,在 constructor 注入剛才設計的環境變數路徑 ENV_PATH,接著設計 setEnvironment 去讀取並解析 .env 檔,然後寫入 config 屬性中,最後設計一個 get(key: string) 的方法來提取要用的環境變數:
import { Inject, Injectable } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import { ENV_PATH } from './constants/token.const';
@Injectable()
export class ConfigurationService {
private config: any;
constructor(
@Inject(ENV_PATH) private readonly path: string
) {
this.setEnvironment();
}
public get(key: string): string {
return this.config[key];
}
private setEnvironment(): void {
const filePath = path.resolve(__dirname, '../../', this.path);
this.config = dotenv.parse(fs.readFileSync(filePath));
}
}
設計完 ConfigurationModule 以後,先在專案路徑下新增 development.env 檔,並設定裡面的內容:
USERNAME=HAO
注意:是新增在專案路徑下,與
package.json同層級,非src。
接著,調整 app.module.ts 的內容:
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigurationModule } from './common/configuration/configuration.module';
@Module({
imports: [
ConfigurationModule.forRoot({
path: `../${process.env.NODE_ENV || 'development'}.env`
})
],
controllers: [
AppController
],
providers: [
AppService
]
})
export class AppModule {
}
調整 app.controller.ts 的內容,在 constructor 注入 ConfigurationService,並改寫 getHello 回傳值:
import { Controller, Get } from '@nestjs/common';
import { ConfigurationService } from './common/configuration/configuration.service';
@Controller()
export class AppController {
constructor(
private readonly configService: ConfigurationService
) {
}
@Get()
getHello() {
return { username: this.configService.get('USERNAME') };
}
}
透過瀏覽器查看 http://localhost:3000,會得到 USERNAME 的值:
Dynamic Module 是非常好用且實用的功能,經常運用在資料庫、環境變數管理等功能,不過需要對 Nest 的依賴注入機制有一定程度的了解,在基礎穩固之後學習上比較不會有問題。這裡附上今天的懶人包:
DynamicModule 型別的物件。DynamicModule 必須包含 module 參數。forRoot 或 register。